package se.curalia.slimcache;


import org.apache.log4j.Logger;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * CacheServlet, disc-based.
 */
public class SlimCache extends HttpServlet {


    public static final Logger log = Logger.getLogger(SlimCache.class.getName());


    Configuration conf;

    protected final HashMap<String, FileFetcher> currentFetchers = new HashMap<String, FileFetcher>();

    public void init() throws ServletException {

        log.info("Slimcache starting up");

        try {
            conf = new Configuration();
        } catch (Exception e) {
            throw new ServletException("Unable to load configuration", e);
        }

        try {
            createBaseDir();
        } catch (IOException e) {
            throw new ServletException("Unable to setup basedir", e);
        }

        log.info("Slimcache started");
    }

    private void createBaseDir() throws IOException {
        String folderName = conf.getBaseDir();
        File folder = new File(folderName);
        if (!folder.exists()) {
            if (!folder.mkdir()) {
                throw new IOException("Cannot create basedir");
            }
        }
        if (folder.exists() && !folder.isDirectory()) {
            throw new IOException("Could not create basedir");
        }
    }


    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        long start = 0;

        if (log.isInfoEnabled()) {
            start = System.currentTimeMillis();
        }

        // TODO: check parameters, throws NP in BBOX as of now.
        try {
            WMSRequest wmsReq = new WMSRequest(req);
            Layer layer = conf.getLayer(wmsReq.getLayers());
            if (layer == null) {
                log.info("No configured layer matched request");
                throw new Exception("No configured layer matches request");
            }
            File tile = getCachedFile(wmsReq);
            if (tile == null) {
                tile = fetchFile(wmsReq, layer);
            } else {
                log.debug("cache hit");
            }
            if (tile == null) {
                log.warn("unable to fetch file");
                throw new ServletException("Unable to fetch tile");
            }
            serializeFile(tile, wmsReq.getImageType(), res);

        } catch (Exception e) {
            log.error(e.toString());
            throw new ServletException(e);
        }
        if (log.isInfoEnabled()) {
            long stop = System.currentTimeMillis();
            log.info("Request time: " + (stop - start) + "ms");
        }
    }

    private File getCachedFile(WMSRequest wmsReq) throws Exception {
        String fileName = createFilename(wmsReq);
        File file = new File(fileName);
        if (file.exists() && file.isFile()) {
            return file;
        } else {
            return null;
        }
    }

    /**
     * Fetches a file.
     * If there already exists a fetcher thread assigned, this method waits for that thread (join).
     *
     * @param wmsReq
     * @param layer
     * @return
     * @throws Exception
     */
    private File fetchFile(WMSRequest wmsReq, Layer layer) throws Exception {
        String uri = createFetchUri(wmsReq, layer);
        String fileName = createFilename(wmsReq);

        // get existing fetcher or create a new.
        // Synchronizing read & creation should ensure
        // no race for creation can appear.
        FileFetcher fetcher = null;
        synchronized (currentFetchers) {
            fetcher = currentFetchers.get(fileName);
            if (fetcher == null) {
                fetcher = new FileFetcher(uri, fileName, wmsReq.getImageType());
                currentFetchers.put(fileName, fetcher);
                fetcher.start();
            }
        }

        // wait for fetcher to finish
        // All requests for a specific tile should wait here.
        fetcher.join();

        // return the file
        return getCachedFile(wmsReq);
    }

    /**
     * Fetcher thread that fetches a file.
     */
    public class FileFetcher extends Thread {
        String uri;
        String fileName;
        String mimeType;

        /**
         * @param uri      uri to connect to.
         * @param fileName filename to store to.
         * @param mimeType mimeType that is expected from the remote service.
         */
        public FileFetcher(String uri, String fileName, String mimeType) {
            super(fileName);
            this.uri = uri;
            this.fileName = fileName;
            this.mimeType = mimeType;
        }

        public void run() {
            if (log.isDebugEnabled()) {
                log.info("Creating fetcher");
            }
            InputStream cis = null;
            FileOutputStream fout = null;
            File fileToCache = null;

            try {
                URL url = new URL(uri);
                URLConnection connection = null;
                connection = url.openConnection();

                // set the configured timeout values
                connection.setConnectTimeout(conf.getConnectTimeout());
                connection.setReadTimeout(conf.getReadTimeout());

                // no real need to do this explicitly, but nice to have a point where it is
                // clear to know that we are connecting
                connection.connect();

                String contentType = connection.getContentType();

                if (!mimeType.equalsIgnoreCase(contentType)) {
                    throw new Exception("ContentType mismatch, request: [" + mimeType + "], remote: [" + contentType + "]");
                }
                fileToCache = new File(fileName);
                // Create directory for the file
                fileToCache.getParentFile().mkdirs();

                // Write the file
                fout = new FileOutputStream(fileToCache);
                cis = connection.getInputStream();
                copyStreams(cis, fout);
                fout.flush();
            } catch (Exception e) {
                log.error(e.getMessage());
                if (fileToCache != null && fileToCache.exists()) {
                	fileToCache.delete();
                }
            } finally {
                if (cis != null) {
                    try {
                        cis.close();
                    } catch (IOException e) {
                        ;
                    }
                }
                if (fout != null) {
                    try {
                        fout.close();
                    } catch (IOException e) {
                        ;
                    }
                }

            }
            // unregister as running fetcher for this tile.
            synchronized (currentFetchers) {
                currentFetchers.remove(fileName);
            }
        }
    }

    private void serializeFile(File file, String mimeType, HttpServletResponse resp) throws Exception {
        OutputStream out = resp.getOutputStream();
        resp.setContentType(mimeType);

        // Set expires header to 6 hours from now
        GregorianCalendar cal = new GregorianCalendar(Locale.ENGLISH);
        cal.setTime(new Date());
        cal.roll(Calendar.HOUR, 6);
        SimpleDateFormat format = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z");
        resp.setHeader("Expires", format.format(cal.getTime()));

        InputStream in = null;
        try {
            in = new FileInputStream(file);
            // Serialize file Content
            copyStreams(in, out);
            out.flush();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (Exception ignore) {
                }
            }
        }
    }

    private void copyStreams(InputStream in, OutputStream out) throws Exception {
        byte[] buffer = new byte[16384];
        int cnt;
        while ((cnt = in.read(buffer)) != -1) {
            out.write(buffer, 0, cnt);
        }
    }

    private String createFetchUri(WMSRequest wmsReq, Layer layer) throws Exception {
        StringBuffer sb = new StringBuffer();
        sb.append(layer.toQueryString());
        sb.append("&");
        sb.append((wmsReq.toQueryString()));
        return sb.toString();
    }

    private String createFilename(WMSRequest wmsReq) {
        StringBuffer sb = new StringBuffer();
        sb.append(conf.getBaseDir());
        sb.append(File.separatorChar);
        sb.append(wmsReq.getLayers());
        sb.append(File.separatorChar);
        sb.append(Math.round(wmsReq.getResolution()));
        sb.append(File.separatorChar);
        sb.append(wmsReq.getBbox().toFilenameString());
        sb.append(".");
        sb.append(conf.getSuffix(wmsReq.getImageType()));
        return sb.toString();
    }

}
